package icasue.tools.sqlhelper.adaptor.ibatis;

import icasue.reflect.annotations.NotNull;
import icasue.reflect.handles.HandleSupplier;
import icasue.reflect.handles.classes.ClassOF;
import icasue.reflect.handles.fields.FieldOF;
import icasue.reflect.handles.method.MethodOF;
import icasue.reflect.handles.predicate.SureOF;
import icasue.reflect.handles.util.OFUtil;
import icasue.tools.sqlhelper.adaptor.annotation.AutoProviderMetadata;
import icasue.tools.sqlhelper.adaptor.annotation.AutoProvider;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2021/11/5 下午2:18
 * @UpdateDate:
 * @Description: 该类用于适配ibatis动态查询SqlProvider机制，
 * 提供 @AutoProviderMetadata 以及 @AutoProvider 注解
 */
public class MybatisAdaptor {

    private static Class MapperProxy;
    private static Class SqlSession;
    private static Class Configuration;
    private static Class Configuration_MybatisPlus;
    private static Class MapperAnnotationBuildr;
    private static Class MappedStatement;
    private static Class ProviderSqlSource;
    private static Class SqlCommandType;
    private static Class BeanFactory;
    private static Class ListableBeanFactory;
    private static Class ProxyStronger;
    private static Class JdkDynamicAopProxy;

    private MethodHandle $mappedStatementSqlSource;
    private MethodHandle $sqlSourceProviderMethodName;
    private MethodHandle $mappedStatementSqlCommandTypeGet;
    private MethodHandle $mappedStatementSqlCommandTypeSet;
    private MethodHandle $get_mapperProxy_sqlSession_configuration;
    private MethodHandle $get_configuration_mappedStatements;
    private MethodHandle $get_plus_configuration_mappedStatements;
    private MethodHandle $get_map_values;

    private final ThreadLocal<AutoProviderMetadata> metadataStoreRef;

    private final static MybatisAdaptor instance = new MybatisAdaptor();

    private MybatisAdaptor(){
        initDependency();
        registerAutoProvider();
        this.metadataStoreRef = refReflectionSqlProvider();
    }

    public static MybatisAdaptor getInstance(){
        return instance;
    }

    // Mapper适配，反射从容器中获取标记@AutoProviderMetadata 注解的MapperProxy, 进行适配.
    public void doAdapt(@NotNull Object listableBeanFactory){
        if(listableBeanFactory == null || !ClassOF.isInstance_.test(ListableBeanFactory, listableBeanFactory)){
            throw new RuntimeException(":: doAdapt :: instance 'org.springframework.beans.factory.ListableBeanFactory' is necessary.");
        }
        try {
            Method getBeansWithAnnotation = ListableBeanFactory.getDeclaredMethod("getBeansWithAnnotation", new Class[]{Class.class});
            Map<String,Object> beanDefined = (Map<String,Object>)getBeansWithAnnotation.invoke(listableBeanFactory, AutoProviderMetadata.class);
            Optional<Object> any = beanDefined.values().stream().findAny();
            if(any.isPresent()) {
                mapperAdapt(any.get());
            }
            // 适配过程针对任意一个标记源信息描述注解的Mapper层进行即可。
            //beanDefined.values().forEach(mapper -> mapperAdapt(mapper));
        }catch (Throwable e){
            /* Perhaps occur Database connection exception. */
            throw new RuntimeException(e.getMessage(), e.getCause());
        }
    }

    // 获取元数据存储 ThreadLocal<AutoProviderMetadata>.
    public ThreadLocal<AutoProviderMetadata> metadataStore(){
        return this.metadataStoreRef;
    }

    // 初始化依赖项.
    private void initDependency(){
        try {
            try {
                MapperProxy = Class.forName("com.baomidou.mybatisplus.core.override.MybatisMapperProxy");
                MapperAnnotationBuildr = Class.forName("com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder");
                Configuration_MybatisPlus = Class.forName("com.baomidou.mybatisplus.core.MybatisConfiguration");
            }catch (Throwable e){
                MapperProxy = Class.forName("org.apache.ibatis.binding.MapperProxy");
                MapperAnnotationBuildr = Class.forName("org.apache.ibatis.builder.annotation.MapperAnnotationBuilder");
            }
            SqlSession = Class.forName("org.apache.ibatis.session.SqlSession");
            Configuration = Class.forName("org.apache.ibatis.session.Configuration");
            MappedStatement = Class.forName("org.apache.ibatis.mapping.MappedStatement");
            ProviderSqlSource = Class.forName("org.apache.ibatis.builder.annotation.ProviderSqlSource");
            SqlCommandType = Class.forName("org.apache.ibatis.mapping.SqlCommandType");
            BeanFactory = Class.forName("org.springframework.beans.factory.BeanFactory");
            ListableBeanFactory = Class.forName("org.springframework.beans.factory.ListableBeanFactory");
            JdkDynamicAopProxy = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        }catch (Throwable e){
            throw new RuntimeException("MybatisAdaptor init depends on ibatis:3.5.0 or higher.", e.getCause());
        }

        try {
            // AutoProvider based on 'icasue-plugin-stronger-advice' or user to implement spring's BeanFactory,
            // then convert BeanDefinition for mapper proxy, is not a smart idea, we dot't suggest!
            ProxyStronger = Class.forName("icasue.stronger.factory.ProxyStronger");
        }catch (Throwable e){
            /* Here meaning user will dealer reProxy's logic by self. */
        }
    }

    // 注册 @AutoProvider 到ibatis的 provider注解处理组.
    private void registerAutoProvider(){
        Field sqlProviderAnnotationSetStoreField = null;
        if(MapperAnnotationBuildr != null){
            try {
                /* using ibatis:[3.5.0 - 3.5.4] */
                sqlProviderAnnotationSetStoreField = ClassOF.getDeclaredField_.apply(
                        MapperAnnotationBuildr, "SQL_PROVIDER_ANNOTATION_TYPES");
                /* in ibatis 3.5.0 - 3.5.4, SqlCommandType not exist will throw exception. */
                changeSqlCommandTypeCodec();
            }catch (Throwable e){
                try {
                    /* using ibatis:[3.5.5 - 3.5.7) */
                    sqlProviderAnnotationSetStoreField = ClassOF.getDeclaredField_.apply(
                            MapperAnnotationBuildr, "statementAnnotationTypes");
                    /* cached mappedStatements getting handle for version:[3.5.5 - 3.5.7] */
                }catch (Throwable e2){
                    throw new RuntimeException("MybatisAdaptor init depends on ibatis:3.5.0 or higher," +
                            " we suggest using [3.5.0 - 3.5.7].");
                }
            }
            cachedHandles();
        }
        if(sqlProviderAnnotationSetStoreField != null){
            Object providerAnnotationSet = FieldOF.get_.apply(sqlProviderAnnotationSetStoreField, null);
            Method $add = ClassOF.getDeclaredMethod_.apply(Collection.class, "add", new Class[]{Object.class});
            MethodOF.invoke_method_invoker_paramArray_.apply($add, providerAnnotationSet, new Object[]{AutoProvider.class});
        }
    }

    // 引用 ReflectionSqlProvider 维护的 metadaStore.
    private ThreadLocal<AutoProviderMetadata> refReflectionSqlProvider(){
        return (ThreadLocal<AutoProviderMetadata>) FieldOF.get_ofTp_fName_instance_.apply(
                ReflectionSqlProvider.class,
                "metadataStore",
                null
        );
    }

    // 缓存相关句柄
    private void cachedHandles() {
        try {
            MethodHandles.Lookup lookup = OFUtil.cast(
                    MethodOF.invoke_method_invoker_paramArray_.apply(
                            MethodOF.findMethod_mType_mName_paramClassArray.apply(HandleSupplier.class, "reFindLookUp", new Class[]{Class.class}),
                            null, new Object[]{MapperProxy}
                    ),
                    MethodHandles.Lookup.class
            );
            /* 获取 (Collection<MappedStatement>) MapperProxy.sqlSession.configuration.mappedStatements 相关句柄. */
            Field field_sqlSession = MapperProxy.getDeclaredField("sqlSession");
            field_sqlSession.setAccessible(true);
            MethodHandle get_sqlSession = lookup.unreflectGetter(field_sqlSession);
            MethodHandle get_configuration = lookup.findVirtual(SqlSession, "getConfiguration", MethodType.methodType(Configuration));
            MethodHandle get_sqlSession_configuration = MethodHandles.filterReturnValue(get_sqlSession, get_configuration);
            $get_mapperProxy_sqlSession_configuration = get_sqlSession_configuration;

            Field field_mappedStatements = Configuration.getDeclaredField("mappedStatements");
            field_mappedStatements.setAccessible(true);
            MethodHandle get_mappedStatements = lookup.unreflectGetter(field_mappedStatements);
            $get_configuration_mappedStatements = get_mappedStatements;

            if(Configuration_MybatisPlus != null){
                Field plus_field_mappedStatements = Configuration_MybatisPlus.getDeclaredField("mappedStatements");
                plus_field_mappedStatements.setAccessible(true);
                MethodHandle plus_get_mappedStatements = lookup.unreflectGetter(plus_field_mappedStatements);
                $get_plus_configuration_mappedStatements = plus_get_mappedStatements;
            }

            MethodHandle getMapValues = lookup.findVirtual(Map.class, "values", MethodType.methodType(Collection.class));
            $get_map_values = getMapValues;

            /* 获取 MappedStatement -> sqlSource 句柄，该句柄 bind MappedStatement，无参，返回 org.apache.ibatis.mapping.SqlSource  */
            Field field_sqlSource = MappedStatement.getDeclaredField("sqlSource");
            field_sqlSource.setAccessible(true);
            MethodHandle get_sqlSource = lookup.unreflectGetter(field_sqlSource);
            $mappedStatementSqlSource = get_sqlSource;

            /* 获取 ProviderSqlSource -> providerMethod -> methodName 句柄，该句柄 bind ProviderSqlSource，无参，返回 java.lang.String  */
            Field field_providerMethod = ProviderSqlSource.getDeclaredField("providerMethod");
            field_providerMethod.setAccessible(true);
            MethodHandle get_providerMethod = lookup.unreflectGetter(field_providerMethod);

            MethodHandle getName = lookup.findVirtual(Method.class, "getName", MethodType.methodType(String.class));
            MethodHandle get_providerMethod_getName = MethodHandles.filterReturnValue(get_providerMethod, getName);
            $sqlSourceProviderMethodName = get_providerMethod_getName;

            /* 获取 MappedStatement -> sqlCommandType 句柄，该句柄 bind MappedStatement，无参，返回java.lang.String */
            Field field_sqlCommandType = MappedStatement.getDeclaredField("sqlCommandType");
            field_sqlCommandType.setAccessible(true);
            MethodHandle get_sqlCommandType = lookup.unreflectGetter(field_sqlCommandType);
            $mappedStatementSqlCommandTypeGet = get_sqlCommandType;

            /* 获取 MappedStatement -> setSqlCommandType 句柄，该句柄 bind MappedStatement, 入参 SqlCommandType, 无返回值. */
            MethodHandle set_sqlCommandType = lookup.unreflectSetter(field_sqlCommandType);
            $mappedStatementSqlCommandTypeSet = set_sqlCommandType;

        }catch (Throwable e){
            e.printStackTrace();
        }
    }

    // 针对ibatis 3.5.0 - 3.5.4 需要先扩展枚举值，加入 {"AUTOPROVIDER" -> SqlCommandType.UNKNOWN} 的映射,
    // 在后续mapperAdaptor时重解析.
    private void changeSqlCommandTypeCodec() throws Throwable{
        Enum unknown = Enum.valueOf(SqlCommandType, "UNKNOWN");
        Object[] enums = SqlCommandType.getEnumConstants();

        Field field_access_values = Class.class.getDeclaredField("enumConstants");
        field_access_values.setAccessible(true);
        Object[] access_values = (Object[]) field_access_values.get(SqlCommandType);
        Object[] access_values_copy = new Object[access_values.length + 1];
        for (int i = 0; i < access_values.length; i++) {
            access_values_copy[i] = access_values[i];
        }
        access_values_copy[access_values.length] = unknown;
        field_access_values.set(SqlCommandType,access_values_copy);


        Field field_access_mapping = Class.class.getDeclaredField("enumConstantDirectory");
        field_access_mapping.setAccessible(true);
        Map<String,Object> access_mapping = (Map<String, Object>) field_access_mapping.get(SqlCommandType);
        Map<String,Object> access_mapping_copy = new LinkedHashMap<>(access_mapping);
        access_mapping_copy.put("AUTOPROVIDER",unknown);
        field_access_mapping.set(SqlCommandType, access_mapping_copy);
    }

    // 修改目标方法 @AutoProvider 其缓存的 SqlCommandType的类型, 使Mybatis正常解析.
    private void mapperAdapt(Object mapperProxy){
        SureOF.notNullAll_.accept(mapperProxy, "Adapt mapperProxy cant be null!");
        mapperProxy = digestMapperProxy(mapperProxy);
        SureOF.equals(ClassOF.isInstance_.test(MapperProxy, mapperProxy), Boolean.TRUE,
                "Adapt mapperProxy's type mistake!");
        try {
            // combine mappedStatements.
            Collection<Object> combineMappedStatements = new ArrayList<>();
            // for mybatis
            if(Configuration != null){
                MethodHandle mappedStatementMapValues = MethodHandles.filterReturnValue(
                        MethodHandles.filterReturnValue(
                                $get_mapperProxy_sqlSession_configuration,
                                $get_configuration_mappedStatements
                        ),
                        $get_map_values
                );
                Collection<Object> mappedStatements = (Collection<Object>)mappedStatementMapValues.bindTo(mapperProxy).invoke();
                combineMappedStatements.addAll(mappedStatements);
            }
            // for mybatis plus.
            if(Configuration_MybatisPlus != null){
                Object plusConfiguration = $get_mapperProxy_sqlSession_configuration.bindTo(mapperProxy).invoke();
                MethodHandle plusMappedStatementMapValues = MethodHandles.filterReturnValue(
                        $get_plus_configuration_mappedStatements,
                        $get_map_values
                );
                Collection<Object> plusMappedStatements = (Collection<Object>)plusMappedStatementMapValues.bindTo(plusConfiguration).invoke();
                combineMappedStatements.addAll(plusMappedStatements);
            }
            for (Object mappedStatement : combineMappedStatements) {
                try {
                    // getting sqlCommandType's name, we will dealing "UNKNOWN"
                    Object sqlCommandTypeName = $mappedStatementSqlCommandTypeGet.bindTo(mappedStatement).invoke().toString();
                    if(!sqlCommandTypeName.equals("UNKNOWN")){
                        continue;
                    }
                    // getting sqlSource, check type of SqlSource to ProviderSqlSource.
                    Object sqlSource = $mappedStatementSqlSource.bindTo(mappedStatement).invoke();
                    if(sqlSource == null || !sqlSource.getClass().equals(ProviderSqlSource)){
                        continue;
                    }
                    // getting providerMethodName, and attends to change SqlCommandType by provider's methodName regex.
                    Object providerSqlSource = ProviderSqlSource.cast(sqlSource);
                    String providerMethodName = (String)$sqlSourceProviderMethodName.bindTo(providerSqlSource).invoke();
                    if(providerMethodName.matches("^(query|count|fieldRow).*$")){
                        // change sqlCommandType to "SELECT"
                        Enum select = Enum.valueOf(SqlCommandType, "SELECT");
                        $mappedStatementSqlCommandTypeSet.bindTo(mappedStatement).invoke(select);
                    }else if(providerMethodName.startsWith("save")){
                        // change sqlCommandType to "INSERT"
                        Enum insert = Enum.valueOf(SqlCommandType, "INSERT");
                        $mappedStatementSqlCommandTypeSet.bindTo(mappedStatement).invoke(insert);
                    }else if(providerMethodName.startsWith("insert")){
                        // change sqlCommandType to "INSERT"
                        Enum insert = Enum.valueOf(SqlCommandType, "INSERT");
                        $mappedStatementSqlCommandTypeSet.bindTo(mappedStatement).invoke(insert);
                    }else if(providerMethodName.startsWith("update")){
                        // change sqlCommandType to "UPDATE"
                        Enum update = Enum.valueOf(SqlCommandType, "UPDATE");
                        $mappedStatementSqlCommandTypeSet.bindTo(mappedStatement).invoke(update);
                    }else if(providerMethodName.startsWith("delete")){
                        // change sqlCommandType to "DELETE"
                        Enum delete = Enum.valueOf(SqlCommandType, "DELETE");
                        $mappedStatementSqlCommandTypeSet.bindTo(mappedStatement).invoke(delete);
                    }else { /* Is not @AutoProvider's method, or method reference is inValid, ignoring. */ }
                }catch (Throwable e){
                    /* Process Here meaning this mappedMethod is not based on SqlProvider, ignore ~ */
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }


    private static Object digestMapperProxy(@NotNull Object mapperProxy){
        if(ProxyStronger == null){
            while (!MapperProxy.isInstance(mapperProxy)){
                if(Proxy.class.isInstance(mapperProxy)){
                    try {
                        mapperProxy = FieldOF.get_ofTp_fName_instance_.apply(Proxy.class, "h", mapperProxy);
                    }catch (Throwable e){
                        /* Based on JDK's proxy, getting InvocationHandler by field "h". */
                    }
                }else if(JdkDynamicAopProxy.isInstance(mapperProxy)){
                    try {
                        Object advised = FieldOF.get_ofTp_fName_instance_.apply(JdkDynamicAopProxy, "advised",mapperProxy);
                        Object targetSource = FieldOF.get_ofTp_fName_instance_.apply(advised.getClass(), "targetSource",advised);
                        mapperProxy = FieldOF.get_ofTp_fName_instance_.apply(targetSource.getClass(), "target",targetSource);
                    }catch (Throwable e){
                        e.printStackTrace();
                        /* Using Aop, need parse instance of JdkDynamicAopProxy. */
                    }
                }else {
                    throw new RuntimeException("Mapper enhancer type unknown.");
                }
            }
        }else {
            while (!MapperProxy.isInstance(mapperProxy)){
                if(Proxy.class.isInstance(mapperProxy)){
                    try {
                        mapperProxy = FieldOF.get_ofTp_fName_instance_.apply(Proxy.class, "h", mapperProxy);
                    }catch (Throwable e){
                        /* Based on JDK's proxy, getting InvocationHandler by field "h". */
                    }
                }else if(ProxyStronger.isInstance(mapperProxy)){
                    try {
                        mapperProxy = FieldOF.get_ofTp_fName_instance_.apply(ProxyStronger, "realInvokeObject", mapperProxy);
                    }catch (Throwable e){
                        /* In direct proxy enhancer by ProxyStronger, getting InvocationHandler by field 'realInvokeObject' */
                    }
                }else if(JdkDynamicAopProxy.isInstance(mapperProxy)){
                    try {
                        Object advised = FieldOF.get_ofTp_fName_instance_.apply(JdkDynamicAopProxy, "advised",mapperProxy);
                        Object targetSource = FieldOF.get_ofTp_fName_instance_.apply(advised.getClass(), "targetSource",advised);
                        mapperProxy = FieldOF.get_ofTp_fName_instance_.apply(targetSource.getClass(), "target",targetSource);
                    }catch (Throwable e){
                        e.printStackTrace();
                        /* Using Aop, need parse instance of JdkDynamicAopProxy. */
                    }
                }else {
                    /* Middle stage proxy, But actually the field is fixed, named 'arg$1'. */
                    try {
                        mapperProxy = FieldOF.get_ofTp_fName_instance_.apply(mapperProxy.getClass(), "arg$1", mapperProxy);
                    }catch (Throwable e){
                        e.printStackTrace();
                    }
                }
            }
        }
        return mapperProxy;
    }

}
